package com.dasberg.gwt.guice;

import com.google.inject.*;
import com.google.inject.binder.LinkedBindingBuilder;
import com.google.inject.binder.ScopedBindingBuilder;
import com.google.inject.multibindings.Multibinder;
import org.apache.log4j.Logger;
import org.scannotation.AnnotationDB;
import org.scannotation.ClasspathUrlFinder;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.Map;
import java.util.Set;

/**
 * The AbstractMultibindModule is a Multibind aware AbstractModule.
 * By default multibinding has to be done manually, which means binding each implementation to
 * the interface. The AbstractMultibindModule allows for binding an interface to classes with a given annotation.
 * Manual binding can still be done too.
 * @author mischa
 */
public abstract class AbstractMultibindModule extends AbstractModule {
    private static final Logger LOG = Logger.getLogger(AbstractMultibindModule.class);

    /**
     * When multibinding to annotated classes, the module will only bind annotated classes that are in the
     * given base package.
     * @return basePackage The base package.
     */
    public abstract String getBasePackage();

    /** Constructor. */
    protected AbstractMultibindModule() {
    }

    /**
     * Returns the AnnotatedMultibinder.
     * @param clazz The interface class.
     * @param <T> The interface.
     * @return binder The annotated multibinder.
     */
    public <T> AnnotatedMultibinder<T> multibind(Class<T> clazz) {
        return new AnnotatedMultibinder(clazz);
    }

    /**
     * Annotated Multibinder.
     * @param <T> The interface class.
     */
    public class AnnotatedMultibinder<T> implements LinkedBindingBuilder<T> {
        private Multibinder multibinder;

        /**
         * Constructor.
         * @param clazz The interface class.
         */
        public AnnotatedMultibinder(Class<T> clazz) {
            multibinder = Multibinder.newSetBinder(binder(), clazz);
        }

        /**
         * Binds all classes that have the given Annotation present to the given interface.
         * @param clazz The annotation class.
         */
        public void toAnnotation(Class clazz) {
            if (clazz.isAnnotation()) {
                URL[] classPaths = ClasspathUrlFinder.findResourceBases(getBasePackage());
                AnnotationDB db = new AnnotationDB();
                try {
                    db.scanArchives(classPaths);
                } catch (IOException e) {
                    LOG.debug("Could not scan archives.");
                }
                Map<String, Set<String>> annotationIndex = db.getAnnotationIndex();
                Set<String> entities = annotationIndex.get(clazz.getName());
                if (entities != null) {
                    for (String entity : entities) {
                        try {
                            multibinder.addBinding().to((Class<? extends T>) Class.forName(entity));
                        } catch (ClassNotFoundException e) {
                            LOG.debug("Could not find class for name: " + entity);
                        }
                    }
                }
            } else {
                throw new IllegalArgumentException("The binding class is not an Annotation.");
            }
        }

        /** {@inheritDoc}. */
        public ScopedBindingBuilder to(Class<? extends T> implementation) {
            return multibinder.addBinding().to(implementation);
        }

        /** {@inheritDoc}. */
        public ScopedBindingBuilder to(TypeLiteral<? extends T> implementation) {
            return multibinder.addBinding().to(implementation);
        }

        /** {@inheritDoc}. */
        public ScopedBindingBuilder to(Key<? extends T> targetKey) {
            return multibinder.addBinding().to(targetKey);
        }

        /** {@inheritDoc}. */
        public void toInstance(T instance) {
            multibinder.addBinding().toInstance(instance);
        }

        /** {@inheritDoc}. */
        public ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
            return multibinder.addBinding().toProvider(provider);
        }

        /** {@inheritDoc}. */
        public ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
            return multibinder.addBinding().toProvider(providerType);
        }

        /** {@inheritDoc}. */
        public ScopedBindingBuilder toProvider(Key<? extends Provider<? extends T>> providerKey) {
            return multibinder.addBinding().toProvider(providerKey);
        }

        /** {@inheritDoc}. */
        public void in(Class<? extends Annotation> scopeAnnotation) {
            multibinder.addBinding().in(scopeAnnotation);
        }

        /** {@inheritDoc}. */
        public void in(Scope scope) {
            multibinder.addBinding().in(scope);
        }

        /** {@inheritDoc}. */
        public void asEagerSingleton() {
            multibinder.addBinding().asEagerSingleton();
        }
    }
}